Podliczanie ceny wycieczki z uwzględnieniem opcji mamy już zrobione. Wykorzystaliśmy do tego przygotowaną wcześniej funkcję calculateTotal. Zanim przejdziemy dalej, warto, żebyśmy szybko omówili jej działanie – dzięki temu lepiej zrozumiemy, jak zapisywać wartości wybranych opcji w stanie aplikacji.
Architektura opcji zamówienia
Przygotowując funkcję calculateTotal założyliśmy, że w stanie aplikacji będziemy mieli obiekt order, a w nim obiekt options. W tym ostatnim, kluczami będą id opcji, a wartościami – to, co wybrał użytkownik (lub, na początku działania aplikacji, domyślne wartości z pricing.json).
W przypadku opcji typu dropdown, icons i number nie będzie problemu – będzie jedna wartość dla każdej opcji. Może to być id wybranej wartości lub liczba w przypadku opcji typu number.
Inaczej będzie funkcjonować opcja typu checkboxes, ponieważ można w niej zaznaczyć kilka wartości. Dlatego w stanie aplikacji zapiszemy tablicę, zawierającą id wszystkich wybranych wartości.
Funkcja calculateTotal iteruje przez wszystkie opcje zapisane w stanie aplikacji, dla każdej nich sprawdzając jej ustawienia w pricing.json. Następnie rozpatrywanych jest kilka warunków w bloku if...else if:
- jeśli wartość opcji w stanie aplikacji jest tablicą, a w
pricing.json jest tablica values dla tej opcji, to bierzemy pod uwagę cenę każdego z zaznaczonych wariantów – ta sytuacja będzie dotyczyła opcji typu checkboxes,
- jeśli wartość opcji w stanie aplikacji nie jest tablicą, ale w
pricing.json jest tablica values dla tej opcji, to znajdujemy ten element z values, który ma id zapisane jako wartość tej opcji w stanie aplikacji (czyli robimy to samo, co powyżej, tyle że bez pętli, bo jest tylko jeden możliwy wybór), i wtedy bierzemy pod uwagę cenę tego wariantu opcji,
- wreszcie, jeśli mamy do czynienia z opcją typu
number, to mnożymy jej wartość przez cenę zawartą w pricing.json dla tej opcji.
Plan komponentów opcji zamówienia
Za chwilę stworzymy komponent OrderOption. Będziemy wykorzystywać go w OrderForm do wyświetlenia pojedynczej opcji. Jednak sam OrderOption nie będzie renderował wyłącznie diva wrappującego opcję oraz nazwę opcji. Do wyświetlenia kontrolki formularza – np. <select> – będzie korzystał z osobnego komponentu.
Potrzebujemy więc komponentu OrderOption oraz po jednym komponencie dla każdego z typów opcji – dropdown, icons, checkboxes i number. Zakładamy jednak, że te subkomponenty będą wykorzystywane tylko i wyłącznie w komponencie OrderOption. Dlatego wprowadzimy nową odmianę struktury plików w naszym projekcie – wszystkie te komponenty cząstkowe będą znajdować się w katalogu src/components/features/OrderOption, razem z komponentem OrderOption.
W ten sposób nieco zmieniamy definicję katalogu OrderOption – nie będzie katalogiem jednego komponentu, ale katalogiem pojedynczej funkcjonalności, na którą może składać się kilka komponentów.
Możesz też pomyśleć o tym inaczej: moglibyśmy pozostać przy jednym komponencie OrderOption. Zawierałby on składnię switch, która – w zależności od typu opcji – zwracałaby np. <select> albo zestaw checkboksów, etc. To by jednak oznaczało, że ten komponent miałby dość skomplikowany i rozbudowany kod. Dlatego wygodniej nam będzie wyeksportować każdy z wariantów tego komponentu do osobnego pliku.
Zacznijmy jednak od podstaw.
Wyświetlanie pojedynczej opcji
Na początku stwórz komponent funkcyjny OrderOption, renderujący diva z klasą styles.component z zaimportowanych styli, w którym umieścimy <h3> z klasą styles.title zawierający nazwę opcji (np. "Car Rental").
Następnie w kodzie JSX w OrderForm.js, przed Col, w którym znajduje się OrderSummary, wstaw mapowanie tablicy pricing (zaimportowanej z pricing.json). Dla każdej opcji z tej tablicy chcemy renderować Col z md={4}, a w nim <OrderOption>, do którego rozpakujemy obiekt tej opcji. Pamiętaj, że musimy też ustawić key dla Col – może mieć wartość id danej opcji.
W rezultacie na stronie powinny wyświetlić się nazwy każdej z opcji.
Wyświetlanie odpowiedniego typu opcji
Stwórz teraz w katalogu komponentu OrderOption cztery proste komponenty funkcyjne, które na początek mają tylko renderować diva z nazwą komponentu. Ich nazwy to:
OrderOptionDropdown,
OrderOptionIcons,
OrderOptionNumber,
OrderOptionCheckboxes.
Następnie zaimportuj je w pliku OrderOption.js. Pod importami, przed kodem tego komponentu, dodaj następujący obiekt:
const optionTypes = {
dropdown: OrderOptionDropdown,
icons: OrderOptionIcons,
checkboxes: OrderOptionCheckboxes,
number: OrderOptionNumber,
};
Kluczami tego obiektu są typy opcji, a wartościami – komponenty, które im odpowiadają. Dzięki temu możemy zmienić kod JSX komponentu OrderOption na:
const OrderOption = ({name, type, ...otherProps}) => {
const OptionComponent = optionTypes[type];
if(!OptionComponent){
return null;
} else {
return (
<div className={styles.component}>
<h3 className={styles.title}>{name}</h3>
<OptionComponent
{...otherProps}
/>
</div>
);
}
};
Wartością stałej OptionComponent będzie jeden z komponentów z obiektu optionTypes. Wykorzystujemy go w kodzie JSX i przekazujemy mu wszystkie propsy otrzymane przez OrderOption, poza name i type. Gdyby z jakiegokolwiek powodu w pricing.json znalazła się opcja typu, który nie jest obsługiwany przez nasz kod, komponent OrderOption zwróci null, czyli niczego nie będzie renderował na stronie.
Jeśli wszystko poszło dobrze, na stronie pod każdą nazwą opcji wyświetla się teraz nazwa komponentu, odpowiadającego za dany typ opcji.
Propsy w komponencie opcji
Zastanówmy się teraz, jakie propsy będą potrzebne naszym nowym komponentom. W OrderForm już przekazujemy do OrderOption wszystkie właściwości danej opcji, które są zapisane w pricing.json – ale to jest tylko konfiguracja. Brakuje nam jeszcze informacji o aktualnej wartości tej opcji (zapisanej w stanie aplikacji) oraz funkcji pozwalającej na zmianę opcji.
Zacznijmy od aktualnej wartości – w tym celu z OrderForm do OrderOption musimy przekazać props currentValue o wartości options[id], gdzie id musisz zamienić na odpowiednie wyrażenie, którego wartość to id danej opcji (np. car-rental).
Pamiętaj, że wcześniej już ustawiamy w stanie aplikacji domyślne wartości dla każdej z opcji, więc props currentValue nigdy nie powinien być undefined (ale może mieć wartość pustego ciągu znaków, albo pustej tablicy). Możesz łatwo sprawdzić, czy ustawiasz prawidłową wartość, za pomocą narzędzi developerskich (zakładka React) – komponent OrderOption dla opcji adults i children powinien domyślnie mieć wartość liczbową, taką samą, jaka została ustawiona dla tych opcji w pricing.json.
Przechodząc do drugiego propsa, otwórz OrderFormContainer.js i dodaj mapowanie dispatchera akcji setOrderOption, zaimportowanej z orderRedux.js, do propsa o tej samej nazwie. To mapowanie ma przyjmować jeden argument i przekazywać go do kreatora akcji (czyli funkcji setOrderOption).
Możemy teraz wrócić do komponentu OrderForm i przekazać do OrderOption ostatni props, czyli setOrderOption – jest to zarówno nazwa propsa w OrderForm, jak i przekazywanego do OrderOption, więc wpiszemy po prostu setOrderOption={setOrderOption}.
Komponenty typów opcji
Za moment będziemy mogli już wykorzystać te żmudnie przekazywane propsy, ale najpierw zmodyfikujemy jeszcze jeden z nich! Przejdź do OrderOption.js. W destrukturyzacji propsów dodaj id i setOrderOption, a dla wykorzystywanego w kodzie JSX komponentu OptionComponent dodaj propsa:
setOptionValue={value => setOrderOption({[id]: value})}
Jest to funkcja strzałkowa, która wywołuje funkcję setOrderOption, przekazując jej obiekt. W tym obiekcie jest jedna właściwość, której kluczem będzie zawartość zmiennej (a w tym wypadku – propsa) id, a wartością – argument funkcji strzałkowej.
Jak pamiętasz, nasza struktura zapisywania wybranych opcji zakłada, że zapisujemy je w obiekcie, w którym kluczami są id opcji. Właśnie dlatego musimy tutaj użyć takiego formatu. Łatwiej nam jest jednak zrobić to raz w tym komponencie niż później z osobna dla każdego z komponentów dla poszczególnych typów opcji.
To wszystko w tym pliku – nie musimy przekazywać currentValue, ponieważ nie używamy tego propsa, więc znalazł się w otherProps, które w całości przekazujemy do subkomponentu.
Opcja dropdown
Czas na komponent OrderOptionDropdown! Na przykładzie tego komponentu omówimy sobie wymagania, które muszą spełniać wszystkie subkomponenty OrderOption*.
const OrderOptionDropdown = ({values, required, currentValue, setOptionValue}) => (
<select
className={styles.dropdown}
value={currentValue}
onChange={event => setOptionValue(event.currentTarget.value)}
>
{required ? '' : (
<option key='null' value=''>---</option>
)}
{values.map(value => (
<option key={value.id} value={value.id}>{value.name} ({formatPrice(value.price)})</option>
))}
</select>
);
Znajdziemy w nim jeden element główny (<select>), który ma klasę odpowiadającą temu komponentowi. Pamiętaj, że style tego komponentu nie mają osobnego pliku .scss, tylko znajdują się w OrderOption.scss.
Wartość tego elementu będzie równa propsowi currentValue, a do eventu zmiany wartości (onChange) przypisaliśmy funkcję strzałkową. Ta funkcja przyjmuje event jako argument i zwraca wywołanie funkcji setOptionValue, której argumentem jest wartość elementu.
Następnie mamy blok kodu, który sprawdza, czy props required jest prawdziwy. Jeśli tak, wstawia pusty ciąg znaków, ale jeśli jest fałszywy (albo nie jest ustawiony), to zostanie wyrenderowany <option> z pustą wartością i tekstem ---. Wykorzystujemy ten zabieg, ponieważ jeśli w pricing.json opcja ma ustawione "required": true, to powinny być do dyspozycji tylko wartości zdefiniowane w tym pliku. Jeśli jednak opcja nie jest wymagana, chcemy dodać <option>, który pozwoli na brak wyboru, czyli rezygnację z tej opcji.
Wreszcie, ostatni fragment kodu to mapowanie po wartościach tej opcji. Dla każdej z nich renderujemy <option>, któremu ustawiamy key, value oraz treść. Bardzo ważne, że wartością tego <option> musi być id danej wartości, ponieważ to po nim rozpoznajemy, która opcja jest wybrana.
Zwróć uwagę, że w treści <option> używamy funkcji formatPrice z utils, aby sformatować cenę tej wartości opcji.
Opcja icons
W tym przypadku zamiast elementu <select> zawierającego wiele <option>, będziemy mieli div zawierający wiele divów. Zewnętrzny div będzie miał tylko className, a wewnątrz niego wykonamy mapowanie po values, w którym wyrenderujemy wewnętrzne divy. Powinny one mieć:
className równy styles.icon oraz styles.iconActive – ale tylko jeśli dany element powinien być aktywny,
- klucz
key,
onClick, w którym funkcja strzałkowa będzie wywoływać funkcję setOptionValue i przekazywać jej id wartości opcji (tej, po której iterujemy),
- w treści:
- komponent
Icon z nazwą ikony z propsów wartości opcji,
- nazwę tej wartości opcji,
- cenę tej wartości opcji, sformatowaną za pomocą funkcji
formatPrice z utils.
Przed mapowaniem dodaj analogicznego, wewnętrznego diva, dla pustej wartości – ale tylko jeśli required jest fałszywe lub nieustawione. Będzie się on różnił od tego użytego w mapowaniu tym, że nie będzie miał klucza key, do funkcji setOptionValue będzie przekazywał pusty ciąg znaków, w treści będzie miał ikonę o nazwie times-circle, a zamiast nazwy będzie tekst "none".
Opcja number
Komponent OrderOptionNumber będzie dużo prostszy – dodaj w nim div z klasą styles.number, w nim wykorzystamy HTML-owy input type="number", więc cała logika działania tego komponentu będzie realizowana przez przeglądarkę. Wystarczy więc ustawić poprawne właściwości tego elementu:
- klasę
styles.inputSmall,
type='number',
value równe propsowi currentValue,
min i max równe odpowiadającym im właściwościom propsa limits,
onChange identyczny jak w przypadku opcji dropdown.
Obok inputa warto dodać też sformatowaną cenę tej opcji.
Opcja checkboxes
Czas na najbardziej wymagający z tego zestawu subkomponentów – OrderOptionCheckboxes. Od strony samego renderowania nie będzie jednak trudny – będzie bardzo podobny do OrderOptionIcons, tyle że:
- zewnętrzny div będzie miał klasę
styles.checkboxes,
- nie będzie opcjonalnego wewnętrznego diva dla pustej wartości,
- w mapowaniu wartości opcji, zamiast diva użyjemy
<label> z kluczem key, w którym znajdzie się:
<input> typu checkbox,
- nazwa oraz cena tej wartości opcji.
Input będzie miał właściwości:
value równe id tej wartości opcji,
checked, które będzie prawdziwe, jeśli to id znajduje się w tablicy currentValue,
onChange, które wywoła funkcję setOptionValue, przekazując jej odpowiednią tablicę wartości (wyjaśnienie poniżej).
Dlaczego piszemy o tablicy currentValue? Dlatego, że może być zaznaczonych kilka wartości tej opcji! Dlatego w stanie aplikacji musimy zapisywać tablicę, której elementami są id poszczególnych wartości opcji, które zostały zaznaczone.
W tym samym pliku, pomiędzy importami a kodem komponentu, dodaj tę funkcję:
const newValueSet = (currentValue, id, checked) => {
if(checked){
return [
...currentValue,
id,
];
} else {
return currentValue.filter(value => value != id);
}
};
Za chwilę wykorzystamy tę funkcję do wygenerowania tablicy, którą zapiszemy w stanie aplikacji dla tej opcji. Najpierw jednak przemyślmy, co robi ta funkcja.
Skoro currentValue jest tablicą, to w momencie zaznaczenia lub odznaczenia checkboksa musimy zmienić tę tablicę. Jeśli checkbox został zaznaczony, to wystarczy dodać id tej wartości opcji do tablicy (robimy to w bloku if). W przeciwnym wypadku musimy usunąć to id z tablicy – ale nie możemy jej modyfikować, tylko musimy stworzyć nową tablicę, co robimy za pomocą metody filter w bloku else.
Wobec tego props onChange w inpucie będzie wykorzystywał tę funkcję, przekazując jej niezbędne informacje:
onChange={event => setOptionValue(newValueSet(currentValue, value.id, event.currentTarget.checked))}
Zakładamy tutaj, że mapując values, nazywasz pojedynczy element value.
Poprawa wyglądu opcji zamówienia
Wreszcie, możemy zająć się wyglądem opcji zamówienia. Wystarczą drobne zmiany w komponencie OrderForm.
Podsumowanie opcji zamówienia
W rezultacie powyższych zmian, na stronie powinny już działać wszystkie opcje zamówienia. Użyj zakładki Redux w narzędziach developerskich, aby zbadać, jakie akcje są wysyłane w momencie zmiany którejś z opcji, i jak wpływają one na stan aplikacji.
Zwróć uwagę, że używamy tego samego komponentu na stronach wszystkich wycieczek – dlatego po przejściu na inną wycieczkę, wybrane wartości opcji powinny być zapamiętane. Zmieni się jednak cena wycieczki, ponieważ każda wycieczka ma inną bazową cenę.